Skip to content

fix(opencode): close webfetch SSRF redirect bypass and harden IP checks#9

Merged
CreatorGhost merged 2 commits into
devfrom
webfetch-ssrf
Jul 12, 2026
Merged

fix(opencode): close webfetch SSRF redirect bypass and harden IP checks#9
CreatorGhost merged 2 commits into
devfrom
webfetch-ssrf

Conversation

@CreatorGhost

@CreatorGhost CreatorGhost commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Selective port of upstream anomalyco/opencode#36377.

What's ported

  • Manual redirect following in webfetch: every hop is re-validated by the SSRF guard (previously a public URL could 302 into private/internal address space).
  • Hardened isPrivateIp: full IPv4 reserved ranges (CGN, TEST-NETs, benchmarking, multicast, broadcast), uncompressed/mixed-notation IPv6 loopback, fe80::/10, mapped-IPv4 forms.
  • OPENCODE_WEBFETCH_ALLOW_PRIVATE=1 escape hatch for intentional internal fetches.
  • New regression suites: webfetch-ssrf.test.ts, webfetch-extract.test.ts.

Deviations from upstream PR

  • Excluded the bundled .opencode/ GitHub triage tooling (upstream repo automation, not relevant to this fork).
  • Excluded permission.ts/wildcard.ts doc-comment-only changes and the semantics.test.ts that documents an older permission API our tree no longer exports.
  • Re-applied two newer upstream deltas the PR predates (webfetch format schema default, LayerNode test wiring).

Verified: 76 tests pass across the three webfetch suites; bun typecheck clean in packages/opencode.

Summary by CodeRabbit

  • Security

    • Strengthened protection against requests to private, localhost, metadata, and other internal addresses.
    • Revalidates every redirect and blocks unsafe or unsupported redirect targets.
    • Enforces the 5 MB response limit while downloading.
  • Reliability

    • Retries Cloudflare challenge responses with an alternate User-Agent.
    • Improved HTML extraction for nested scripts, styles, frames, and other skipped content.
  • Tests

    • Added coverage for SSRF protection, redirects, response handling, and HTML extraction.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@CreatorGhost, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a0052f1-0957-44f7-a468-1207a1feb702

📥 Commits

Reviewing files that changed from the base of the PR and between 2eeb698 and ec81ae4.

📒 Files selected for processing (2)
  • packages/opencode/src/tool/webfetch.ts
  • packages/opencode/test/tool/webfetch.test.ts
📝 Walkthrough

Walkthrough

Webfetch adds SSRF validation for URLs and DNS results, manually validates redirects, retries Cloudflare challenges, enforces streaming response limits, and exports corrected HTML extraction logic with focused tests.

Changes

Webfetch security and extraction

Layer / File(s) Summary
Public URL validation
packages/opencode/src/tool/webfetch.ts, packages/opencode/test/tool/webfetch-ssrf.test.ts
Adds private IPv4/IPv6 detection, hostname checks, DNS-result validation, and tests for private and public targets.
Fetch execution controls
packages/opencode/src/tool/webfetch.ts, packages/opencode/test/tool/webfetch.test.ts
Adds context-controlled private fetching, validated manual redirects, Cloudflare challenge retry, streaming size enforcement, and related execution tests.
HTML extraction behavior
packages/opencode/src/tool/webfetch.ts, packages/opencode/test/tool/webfetch-extract.test.ts
Exports extractTextFromHTML, corrects skipped-tag depth tracking, and tests tag removal and nested skip regions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebFetchTool
  participant URLValidation
  participant WebServer
  WebFetchTool->>URLValidation: validate initial URL
  WebFetchTool->>WebServer: fetch with manual redirect handling
  WebServer-->>WebFetchTool: response or redirect
  WebFetchTool->>URLValidation: validate redirect target
  WebFetchTool->>WebServer: follow validated redirect
  WebServer-->>WebFetchTool: stream response body
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the main changes and verification, but it omits required template sections like the issue link, type of change, and checklist. Add the missing template sections: issue reference, type of change, checklist, and any screenshot note if applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: webfetch SSRF redirect hardening and private IP checks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch webfetch-ssrf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CreatorGhost

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 44 minutes.

@CreatorGhost

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/opencode/src/tool/webfetch.ts (1)

105-131: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid leaking resolved private IP in the thrown error, and prefer a functional check over the manual loop.

record.address (the internal-network IP a hostname resolves to) is embedded directly in the error message, which can propagate back to an untrusted caller (e.g. an LLM agent relaying tool output) and aid network reconnaissance. Also, the for loop over records can be replaced by .some()/.find() per the array-methods guideline.

🔒 Proposed fix
   const records = await dns.lookup(host, { all: true })
-  for (const record of records) {
-    if (isPrivateIp(record.address)) {
-      throw new Error(
-        `Refusing to fetch host that resolves to a private address: ${host} -> ${record.address}. ${ALLOW_PRIVATE_HINT}`,
-      )
-    }
-  }
+  if (records.some((record) => isPrivateIp(record.address))) {
+    throw new Error(`Refusing to fetch host that resolves to a private address: ${host}. ${ALLOW_PRIVATE_HINT}`)
+  }

As per coding guidelines, "Prefer functional array methods over for loops, and use type guards on filter to preserve downstream inference."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/webfetch.ts` around lines 105 - 131, Update
assertPublicUrl to replace the manual records loop with a functional check such
as some or find, while preserving rejection when any resolved address is
private. Remove record.address from the thrown error message so it reports only
the hostname and generic refusal context without exposing resolved IPs.

Source: Coding guidelines

packages/opencode/test/tool/webfetch.test.ts (1)

124-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Missing regression test for the PR's core fix: redirect hop pointing to a private target.

The three new tests cover redirect-follow-to-public, redirect-limit, and initial-URL rejection, but none exercises a redirect chain where an intermediate hop's location points at a private/loopback address. That's the exact bypass this PR closes (public URL 302s to an internal address), so it deserves direct coverage rather than relying on the initial-URL check alone.

✅ Suggested additional test
it.instance("rejects redirects to private targets when the SSRF guard is enabled", () =>
  withFetch(
    (req) => {
      const path = new URL(req.url).pathname
      if (path === "/start") return new Response(null, { status: 302, headers: { location: "http://127.0.0.1/" } })
      return new Response("landed", { status: 200 })
    },
    (url) =>
      Effect.gen(function* () {
        const exit = yield* Effect.exit(
          exec({ url: new URL("/start", url).toString(), format: "text" }, { allowPrivate: false }),
        )
        expect(exit._tag).toBe("Failure")
      }),
  ),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/tool/webfetch.test.ts` around lines 124 - 162, Add a
regression test alongside the existing redirect tests that starts from a public
`/start` URL, returns a redirect whose `location` targets `http://127.0.0.1/`,
and executes `exec` with `allowPrivate: false`. Assert via `Effect.exit` that
the result is a failure, covering SSRF validation of redirect destinations
rather than only the initial URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/tool/webfetch.ts`:
- Around line 170-182: Replace the current assertPublicUrl-only protection in
the web fetch flow with a transport-layer HttpClient/dispatcher that resolves
and validates the target, then pins and reuses the validated address for the
connection and redirect hops. Integrate this custom transport into the fetch
request path while preserving the allowPrivate bypass and existing error
handling.

---

Nitpick comments:
In `@packages/opencode/src/tool/webfetch.ts`:
- Around line 105-131: Update assertPublicUrl to replace the manual records loop
with a functional check such as some or find, while preserving rejection when
any resolved address is private. Remove record.address from the thrown error
message so it reports only the hostname and generic refusal context without
exposing resolved IPs.

In `@packages/opencode/test/tool/webfetch.test.ts`:
- Around line 124-162: Add a regression test alongside the existing redirect
tests that starts from a public `/start` URL, returns a redirect whose
`location` targets `http://127.0.0.1/`, and executes `exec` with `allowPrivate:
false`. Assert via `Effect.exit` that the result is a failure, covering SSRF
validation of redirect destinations rather than only the initial URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e148c35-1f09-48c4-a800-573531583df9

📥 Commits

Reviewing files that changed from the base of the PR and between 67e2bd4 and 2eeb698.

📒 Files selected for processing (4)
  • packages/opencode/src/tool/webfetch.ts
  • packages/opencode/test/tool/webfetch-extract.test.ts
  • packages/opencode/test/tool/webfetch-ssrf.test.ts
  • packages/opencode/test/tool/webfetch.test.ts

Comment on lines +170 to +182
// SSRF guard: block loopback / link-local / private targets. Redirect
// hops are re-validated in the fetch loop below. Remaining known gap:
// DNS rebinding (a hostname that resolves public here but private when
// the client re-resolves it) — closing it fully requires pinning the
// validated IP into the connection, which fetch does not expose.
const allowPrivate = yield* AllowPrivateFetch
if (!allowPrivate) {
yield* Effect.tryPromise({
try: () => assertPublicUrl(params.url),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does effect's FetchHttpClient or undici support pinning a resolved IP address (custom DNS lookup) per request to prevent DNS rebinding?

💡 Result:

Yes, it is possible to pin a resolved IP address in undici to prevent DNS rebinding [1][2][3]. While Effect's FetchHttpClient layer is designed for environments like Bun, browsers, or the global Node.js fetch [4], the underlying undici library (used by Node.js or when using NodeHttpClient) provides the necessary control via its Agent configuration [5][6]. To pin an IP address and close the DNS-rebinding time-of-check to time-of-use (TOCTOU) gap, you must provide a custom lookup function within the connect options of an undici Agent [1][7][3]. This hook allows you to perform your own DNS resolution (or use dns.lookup), validate the IP address (e.g., against a blocklist of private/internal ranges), and then return that validated IP so the socket connects directly to it, bypassing any further resolution [1][8]. Example implementation: import { Agent } from 'undici'; import dns from 'node:dns'; // Create a custom Agent with a pinned DNS lookup const safeAgent = new Agent({ connect: { lookup: (hostname, options, callback) => { // 1. Perform custom resolution dns.lookup(hostname, options, (err, address, family) => { if (err) return callback(err, address, family); // 2. Validate address (e.g., check for private IP ranges) if (isUnsafe(address)) { return callback(new Error('SSRF blocked'), address, family); } // 3. Return the validated, pinned address callback(null, address, family); }); }, }, }); // Use the agent for your request // const client = new Client(origin, { dispatcher: safeAgent }); // or // await fetch(url, { dispatcher: safeAgent }); Note that if you are using Effect, you should ensure you are utilizing the undici-compatible HTTP client (e.g., NodeHttpClient.layerUndici) rather than the generic FetchHttpClient layer, as the latter might rely on the global fetch environment where dispatcher injection may be restricted or have different compatibility characteristics [9][4][3]. Using undici directly allows you to control the socket-level connection details required for robust SSRF defense [1][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby redirect/URL validation logic.
sed -n '1,320p' packages/opencode/src/tool/webfetch.ts

# Find the HTTP client implementation and any undici/custom-dispatcher usage.
rg -n "FetchHttpClient|NodeHttpClient|undici|dispatcher|lookup|assertPublicUrl|redirect" packages/opencode/src -S

Repository: CreatorGhost/TheCode

Length of output: 24147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant module shapes before reading specific slices.
ast-grep outline packages/opencode/src/tool/webfetch.ts --view expanded || true
ast-grep outline packages/opencode/src -g '*.ts' --view expanded | head -n 200 || true

Repository: CreatorGhost/TheCode

Length of output: 1171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe whether a concrete dispatcher/lookup hook is available in the codebase.
rg -n "new Agent|lookup:|custom.*lookup|setGlobalDispatcher|Agent\(" packages/opencode/src -S

Repository: CreatorGhost/TheCode

Length of output: 1874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the package uses Effect's FetchHttpClient or NodeHttpClient elsewhere.
rg -n "FetchHttpClient|NodeHttpClient|layerUndici|HttpClient" packages/opencode/src packages/opencode -g '*.ts' -S

Repository: CreatorGhost/TheCode

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read package dependency info if available to infer runtime/fetch backend.
cat package.json 2>/dev/null || true
find packages/opencode -maxdepth 2 -name package.json -print -exec sed -n '1,220p' {} \;

Repository: CreatorGhost/TheCode

Length of output: 11129


🌐 Web query:

Effect unstable/http FetchHttpClient.RequestInit dispatcher custom lookup undici pin IP

💡 Result:

To pin an IP address or use a custom DNS lookup in Node.js using Undici, you must provide a custom lookup function within the connect options of a dispatcher (such as Client, Pool, or Agent) [1][2]. The standard approach involves creating an instance of a dispatcher with a connect configuration that overrides the default DNS lookup behavior [2]. Example using Undici Client: import { Client } from 'undici'; import dns from 'node:dns'; const client = new Client('https://example.com', { connect: { lookup: (hostname, options, callback) => { // Pin to a specific IP address callback(null, [{ address: '1.2.3.4', family: 4 }]); } } }); In the context of the Effect framework (e.g., @effect/platform/FetchHttpClient), you can provide a custom dispatcher by configuring the underlying HTTP client [3]. Because FetchHttpClient relies on Undici for its implementation, you can instantiate an Agent or Client with the custom connect options shown above and pass it to your fetch requests or provide it via the Effect layer [1][3]. Note that the dispatcher option in fetch or RequestInit is used to override the default global dispatcher [4]. Ensure that when you use a custom dispatcher, it is configured correctly to handle the requirements of your specific application [5]. The lookup function signature follows the standard Node.js dns.lookup callback pattern [6][7].

Citations:


🌐 Web query:

Bun fetch dispatcher lookup undici custom Agent DNS rebinding

💡 Result:

In Bun, using a custom Undici Agent to control DNS resolution—a common technique to prevent DNS rebinding—has historically faced compatibility issues because Bun's native fetch did not always fully respect custom dispatchers or their connect functions [1][2]. However, as of August 2025, Bun has implemented support for custom connect functions within the dispatcher option for fetch [3]. This improvement ensures that when you provide an Undici Agent (configured with a custom connect lookup) to the fetch dispatcher option, Bun correctly utilizes that logic to resolve hostnames [3]. To implement a DNS-rebinding-safe client in Bun, you can follow this pattern: 1. Perform your own DNS lookup for the target hostname [4]. 2. Validate the resolved IP address against your security policy (e.g., ensuring it is not a private or loopback address) [4]. 3. Create an Undici Agent with a custom connect.lookup function that returns your pre-validated IP address [4]. 4. Pass this Agent as the dispatcher option to your fetch request [4]. Example implementation: import { Agent } from "undici"; import dns from "dns/promises"; // 1. Resolve and validate IP manually const { address } = await dns.lookup("example.com"); // 2. Define custom dispatcher const dispatcher = new Agent({ connect: { lookup: (_hostname, _options, cb) => { // Force the agent to use the validated IP cb(null, address, 4); }, }, }); // 3. Use in fetch const response = await fetch("https://example.com", { dispatcher }); While Bun continues to improve its Undici compatibility to support libraries like fetch-socks or custom agents [5][2], always verify your specific Bun version if you encounter unexpected behavior, as some features were implemented recently as part of maintaining ecosystem compatibility [3][5].

Citations:


🌐 Web query:

"effect/unstable/http" "FetchHttpClient.RequestInit" RequestInit service dispatcher

💡 Result:

In Effect, FetchHttpClient is an implementation of the HttpClient service provided by the @effect/platform package [1][2]. The RequestInit configuration for FetchHttpClient allows you to customize the underlying Fetch API request settings [3][1]. To customize the FetchHttpClient, you provide a configuration object using the FetchHttpClient.RequestInit tag [1][2]. This is typically done by creating a Layer that provides this configuration to the runtime [1][2]. Example usage: import { FetchHttpClient, HttpClient } from "@effect/platform" import { Effect, Layer } from "effect" const CustomFetchLive = FetchHttpClient.layer.pipe( Layer.provide( Layer.succeed(FetchHttpClient.RequestInit, { credentials: "include", cache: "force-cache" // Example of customizing cache behavior [4] }))) const program = Effect.gen(function* { const client = yield* HttpClient.HttpClient const response = yield* client.get("https://jsonplaceholder.typicode.com/posts/1") const json = yield* response.json console.log(json) }).pipe( Effect.scoped, Effect.provide(CustomFetchLive)) Regarding a "service dispatcher," the HttpClient service in Effect functions as an abstraction over HTTP operations [5]. It does not typically use a "dispatcher" by that specific name in its public API; rather, it uses the Effect environment (Context) to provide the implementation (e.g., FetchHttpClient) and uses standard Effect piping and layering for dependency injection [1][2][5]. The HttpClient interface provides methods like get, post, put, and del, which are executed using the provided client implementation [5][6]. If you require custom dispatching or middleware logic, you can use HttpClient.mapRequest, HttpClient.mapRequestEffect, or create custom Layers that wrap or augment the base HttpClient implementation [5][6][7].

Citations:


🌐 Web query:

site:effect.website FetchHttpClient RequestInit dispatcher undici

💡 Result:

In the context of the Effect ecosystem, particularly within @effect/platform-node, the undici library is utilized as a backend for HTTP requests [1]. When using FetchHttpClient, which serves as an implementation of the HttpClient interface, the underlying communication is handled by undici [1][2]. Regarding the use of dispatcher with FetchHttpClient and RequestInit: 1. Undici Dispatcher: The undici library uses a Dispatcher to manage HTTP requests. When FetchHttpClient is employed, it is typically configured via a layer (e.g., FetchHttpClient.layer or layerUndici) [3][4]. If you need to customize the undici client (such as providing a specific dispatcher), this is generally handled at the layer creation level when configuring the HttpClient service, rather than through individual RequestInit objects [3][2]. 2. RequestInit: The standard RequestInit interface (used by the native fetch API) does not natively support an undici-specific dispatcher property. RequestInit is intended for standard fetch options like method, headers, and body [5]. 3. Implementation Detail: Because FetchHttpClient is an Effect-based abstraction, configuration that requires underlying engine details—like a custom undici.Dispatcher—is managed through Effect's dependency injection system (Layers) [3][2]. You should configure the HttpClient layer with your required undici settings (like a custom Agent or Dispatcher) when providing the layer to your application, ensuring that all requests made through the injected HttpClient utilize the desired configuration [3][2]. If you are encountering issues where a global undici dispatcher is not being picked up or needs to be explicitly defined, verify that your layer configuration in the Effect program correctly injects the HttpClient with the undici client configured to your specifications [6][4].

Citations:


Close the DNS-rebinding gap at the transport layer. assertPublicUrl() only validates one DNS answer before the request; the fetch path can still re-resolve for the actual connection and each redirect hop. FetchHttpClient.RequestInit doesn’t expose per-request IP pinning, so this needs a custom HttpClient/dispatcher layer that reuses the validated address for the socket.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/webfetch.ts` around lines 170 - 182, Replace the
current assertPublicUrl-only protection in the web fetch flow with a
transport-layer HttpClient/dispatcher that resolves and validates the target,
then pins and reuses the validated address for the connection and redirect hops.
Integrate this custom transport into the fetch request path while preserving the
allowPrivate bypass and existing error handling.

- Replace manual for loop with records.some() in assertPublicUrl
- Remove resolved IP from error message to prevent info leak
- Add regression test for redirect-to-private-target SSRF bypass
@CreatorGhost

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@CreatorGhost CreatorGhost merged commit dbd7ccd into dev Jul 12, 2026
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant